Skip to content

fix(0228): scale the pivot by the measured USDC/USD rate, pivot-leg reset tooling, XLM snapshot - #311

Merged
adamkoot merged 12 commits into
developfrom
fix/0228_xlm-oracle-rate-is-measured-then-discarded-while-the-pivot-derives-it
Sep 14, 2026
Merged

adamkoot merged 12 commits into
developfrom
fix/0228_xlm-oracle-rate-is-measured-then-discarded-while-the-pivot-derives-it

Conversation

@adamkoot

@adamkoot adamkoot commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • pivot_sql now multiplies the ref/USDC vwap by the measured USDC/USD rate from usd_rate at the bucket end (oracle over external via two nested method-specific ASOF legs, no argMax); a bucket with no rate is left unpriced, never close × 1. Fixes the last stored prices still assuming USDC = $1 after 0268 — every pre-epoch XLM- and USDT-quoted candle (2023-03-11 stored +3.2 %).
  • New pivot-leg reset mode (--reset-require-pivot-usdc-rate) for the re-enrichment campaign: one predicate at three sites, five refusals (USDC leg, non-pivot leg, oracle shadow, empty day set, both modes at once), value-idempotent, price_ohlcv_1m still refused; runbook Appendix C and the post_run_0228_it falsifier. Dry run now runs the pivot-window guard too (review WR-01).
  • measured_identities() beside an untouched peg_identities() snapshots XLM Reflector readings into usd_rate as oracle/hops 0 through a second, separately non-fatal snapshot call; the identity evidence is a test.
  • Phase-0 measurement on prod (8 read-only queries, in notes/) rescoped the task: AC 1 as filed is met (pivot vs Reflector ±35 bps), the oracle tier already prices XLM legs inside its window, retention is the dark cleanup-worker's policy, not a TTL.
  • 724 CI tests pass, clippy -D warnings clean. The #[ignore] ClickHouse suite HAS run, against a rootless clickhouse-common-static-26.3.10.60 (the prod build): ch_enrich_it 44/44, usd_rate_population_it 5/5, plus a hand-built Appendix C campaign with the real coarse-repair binary on all six tables (0.588 → 0.5692428 = ×0.9681 on 2023-03-11, controls untouched, rerun value-idempotent at version 5) and the XLM snapshot path through the real writer.
  • That prove run found and fixed two defects (commits afe587e, c768b27): the post_run_0228_it acceptance gate decoded a Nullable(Float64) factor into f64 and reported ±1e230 on a correctly repaired table — it could never have passed on prod (now Option<f64> like the 0268 twin, NULL is a harness finding, CI-pinned); and the ResetRequiresExternalRates refusal both --reset-require-* flags promised was unreachable through the CLI (an unloaded series empties the month enumeration → exit 0, "0 month(s)") — CoarseRepairDriver::run now checks it before enumerating months, dry run included, #[ignore]-tested and confirmed with the binary. Task README Issues 8–10 and runbook Appendix B/C updated; a refused run still leaves its FREEZE behind locally (Issues 10, spawn list 4, prod unaffected).
  • /code-review on the branch: 3 findings, all fixed in 9fd74f3/fe452ab, no defect in pivot_sql. Metric: the snapshot counts were summed into OracleUsdRatesSnapshotted, hiding a stalled USDC snapshot behind XLM's rows — now per set (peg series unchanged in meaning, new OracleMeasuredRatesSnapshotted). Gate: the pivot-leg reset passed with canonical USDC missing from prices.assets and zeroed a leg no pivot refills — now requires can_pivot(). Dry run: both leg refusals (0228 and 0268 modes) ran only in the per-month pass — now before month enumeration. ch_enrich_it 46/46 on 26.3.10.60, 725 CI tests.
  • The campaign and the deploy are the operator's — task stays active, a CHORE follows on develop. Deploy is Prices-production-EventBridge (enrichment + oracle Lambdas), not Compute.

The pivot tier's ref_usd is a price in USDC, not in dollars, so every XLM-
and USDT-quoted candle carried the USDC = $1 assumption task 0268 had just
removed from the USDC leg itself. USDC closed at 0.9681 on 2023-03-11, so the
stored value is ~3.2% high across the whole pre-epoch pivot population.

pivot_sql now ASOF-joins prices.usd_rate for canonical USDC at the candidate's
bucket END and writes close_usd as close x vwap(ref/USDC) x usdc_usd. The two
method-specific rate legs are NESTED, as queries_ch::peg_series_sql proves, so
a valid oracle reading wins outright without an argMax across methods. A bucket
with neither rate is left unpriced rather than written as 0 x close.

The candidate scan moved into a subquery because ASOF needs a materialized
column for its inequality, which moves the partition bound onto the bare column
and reorders the first two binds.
…d runbook Appendix C

The scaled pivot only reaches rows still at close_usd = 0, so the ~118 M
pre-epoch XLM-quoted and ~1.6 M USDT-quoted candles already carrying an
unscaled value are inert. This adds the operator mechanism that re-opens them.

UsdResetSpec gains require_pivot_usdc_rate, which appends the SAME
external_rate_day_pred the 0268 mode uses and nothing else: a pivoted row never
carried the peg tier's close_usd = close signature, so including it would select
nothing. That also means the mode cannot be a fixed point across runs — it is
value-idempotent instead, which is a recorded deviation from BRIEF decision C.

The mode refuses the canonical USDC leg (that one is 0268's), a non-pivot leg,
an oracle-shadowed span, a zero day-set, an empty window, and the two modes
together. It is not gated on hourly rates: that refusal guards an
irreversibility only the par signature creates.

Appendix C carries the preconditions with their STOP conditions, the per-leg
--reset-not-before derivation, the dry-run gate, the baseline, the expected
runtime and the rollback. post_run_0228_it.rs is the operator's after-check,
expected to fail until the campaign runs.
XLM's 52,607 Reflector readings live only in oracle_prices, which the dark
cleanup worker's 13-month policy will eventually prune, while usd_rate — never
pruned — holds none of them. peg_identities() is exactly canonical USDC by
design, and correctly so: XLM is not a peg.

measured_identities() is a second set beside it, named for what its members are:
measured, non-peg references. Its doc comment carries the identity evidence and
a test calls reflector_key_to_identity("XLM") so the claim is code rather than
prose, to task 0267's standard.

The two sets go to populate_usd_rate_from_oracle as SEPARATE calls. Its task
0139 identity pre-pass runs over the whole slice and returns before writing
anything for any identity, so appending XLM to the peg call would let a
collision on XLM's asset_id silently stop USDC's snapshot too.

The copy is gap-filling rather than watermarked, so the first run after deploy
copies all existing readings and no backfill tool is needed.
…ator checklist

Implementation notes for the three commits, design decisions (D-01..D-09 from
plan, 11 emerged), the D-06 deviation from BRIEF decision C with its evidence,
the broken/modified test list, the acceptance-criteria status split between the
branch and the operator run, Appendix C's checklist and the spawn list.

Records that the #[ignore] ClickHouse tests were written but not run — no
ClickHouse is reachable on this machine — and keeps the read-only prod probe
that retired RESEARCH assumption A2.
Review WR-01: coarse-repair gated the --pivot-window-s minimum-width guard
behind !dry_run, so Appendix C's "dry run first" would accept a window the
real run then refuses. The window guards now run in dry-run mode as well;
only the snapshot guard stays real-run-only, since a dry run discards
nothing. Inherited from the 0182 shape. The task file records the review
and verification results.
Two defects found by the prove run against ClickHouse 26.3.10.60.

post_run_0228_it decoded a Nullable(Float64) ratio into f64 — the
reference vwap's nullIf makes the whole median nullable — so RowBinary
landed one byte off and the acceptance gate reported a carried factor
of -3.9e230 on a table the tool had just repaired to 0.9681. It could
never have passed on prod. The field is Option<f64> now, as in the
0268 twin; NULL behind matched rows is a finding that names the
harness, and CI pins it.

The ResetRequiresExternalRates refusal both --reset-require-* flags
promised was unreachable through coarse-repair: an unloaded series
does not fail the month enumeration, it empties it, so the run ended
green with "0 month(s)". CoarseRepairDriver::run now checks it first,
dry run included, through the one fn the pass already uses.
The #[ignore] ClickHouse suite was recorded as "written, NOT RUN"; it
has now run against the prod build, together with a hand-built
Appendix C campaign on all six tables and the XLM snapshot path. The
task README carries the results, the two defects (Issues 8-9), the
FREEZE-after-refusal caveat (Issues 10, spawn list 4) and decisions
21-22. Appendix B and C say what the no-rates refusal now actually
does, and C warns about the snapshot a refused run leaves behind.
Three code-review findings, each reproduced before the fix.

OracleUsdRatesSnapshotted summed the peg and measured snapshots. XLM
lands rows every pass, so a stalled USDC snapshot, the pivot's only
post-epoch rate and non-fatal by design, could never read zero. The
series is peg-only again and XLM gets OracleMeasuredRatesSnapshotted.

The pivot-leg gate checked pivot_ids() alone, while the pivot only
runs when canonical USDC resolves in prices.assets. With USDC missing
there and its rates loaded, every gate passed and the reset zeroed a
leg no pivot refilled (rows_reset 1, rows_enriched 0). It now requires
can_pivot().

Both leg refusals ran only inside the per-month pass, which a dry run
never builds, so rehearsing the wrong leg ended green. The driver now
runs them, with the no-rates check, before enumerating months.
Appendix B and C now list each reset refusal where it actually fires:
before connecting, before month enumeration, or inside the pass, which
is where the FREEZE-left-behind caveat still applies. The task README
records the review (Issues 11), decisions 22-24 and the superseded
WR-02 note.
@karczuRF

Copy link
Copy Markdown
Collaborator

Code review — correctness pass over the diff

Four findings. The SQL itself checks out: bind order matches the new render order (candidate watermark → reference watermark → pivot_window_s → LIMIT), the two rate legs are nested the way queries_ch::peg_series_sql already proves works under both analyzers, toUInt32(bend) - toUInt32(rts) fails closed on an unmatched ASOF (1970 default), the INSERT_COLUMNS projection stays positionally aligned, and the reset's day-set predicate never over-reaches the tier's bucket-end staleness on any grain (checked _15m/_1h/_4h/_1d/_1w/_1M individually — the tier's window always contains the bucket's start day).


1. packages/enrichment-worker/src/ch_enrich.rs:2556 — the scheduled pass now depends on usd_rate for all pivot-leg pricing, and its only post-epoch supplier fails non-fatally

WHERE {rate} > 0 makes every XLM- and USDT-quoted candle require a canonical-USDC rate within max(1 day, bucket width) of its bucket end. stable_ids() is USDC-only, so these legs have no tier below the pivot — a bucket with no rate stays at close_usd = 0.

This is not campaign-scoped: it is the hourly price_ohlcv_1m Lambda too. Post-epoch the only supplier of those rows is populate_usd_rate_from_oracle(&peg_identities(), …), which lib.rs:617 deliberately treats as non-fatal (logged, plus OracleUsdRatesSnapshotted, which has no alarm — the docstring says so explicitly).

Concrete failure: the peg snapshot starts erroring (the task-0139 pre-pass trips because a second identity lands on canonical USDC's asset_id, or the ~2%-of-invocations oracle OOM becomes persistent). 24 h later the peg-pivot tier stops writing close_usd for every XLM- and USDT-quoted candle. Nothing fails: the tier's own no-progress branch is warn!, and the run reports a clean pass. The rows land at close_usd = 0, which the diff's own comment notes ~130 unguarded argMax(close_usd, …) sites read as a real price.

That the existing integration fixtures (ch_enrich_it.rs:163, :1157, setup_0182) all had to gain seed_external_rate(…, 1.0) to keep passing is the same behaviour observed from the other side.

Suggest either an alarm on OracleUsdRatesSnapshotted sustained at 0 while OracleRowsWritten climbs (the shape OracleStats::rates_snapshotted already names as the failure), or making that snapshot fatal to the pass now that a downstream tier depends on it.


2. packages/enrichment-worker/src/ch_enrich.rs:1127 — the WR-01 dry-run fix is partial, and it misses the refusal the runbook calls the most likely blocker

assert_reset_leg_and_rates hoists only the leg check and the rates-loaded check ahead of the month enumeration. reset_step (:1319) still runs four more refusals that a dry run never reaches, because a dry run never builds a pass:

  • assert_reset_not_shadowed_by_oracle — applies to every spec, 0228 included;
  • assert_reset_target_is_priceable — every spec;
  • assert_hourly_rates_are_loaded, assert_no_pre_epoch_oracle_rows — the 0268 mode.

The first is the one Appendix C precondition 5 flags as "BLOCKING, and this is the one most likely to stop you": XLM has held Reflector readings since 2026-03-11 and --reset-not-after defaults to 14:00 that day, so a single earlier XLM reading refuses the campaign. Scenario: the operator dry-runs --table price_ohlcv_1d … --reset-require-pivot-usdc-rate, it lists months and exits green; the real run FREEZEs partition 1 and then dies with ResetBlockedByOracleRows. That is exactly the rehearsal-of-nothing this change was written to remove, plus the orphaned FREEZE the task README lists as Issue 10.

The docstring's "the same methods reset_step calls, so the two cannot drift" is not true as written — they are two independent lists. Worth either hoisting the window-independent refusals wholesale, or having reset_step call assert_reset_leg_and_rates() so there is one list.


3. packages/enrichment-worker/src/ch_enrich.rs:509 — the 0228 candidate set carries no reference-market term, so it can zero rows the pivot cannot reach

The 0228 branch appends external_rate_day_pred and nothing else, which is right for the rate but says nothing about the reference. pivot_sql additionally requires r.usd IS NOT NULL AND (p.timestamp - r.timestamp) <= pivot_window_s.

So: a bucket on a rate-covered day whose XLM/USDC (or USDT/USDC) market has no candle within --pivot-window-s before it is re-opened by the reset and then not refilled — the 0182 shape. Dropping the par signature widens this population a lot compared with the 0268 mode: 0268's candidate was self-erasing and narrow, this one is every written row of the leg on a covered day, across 12 passes over deep history where the reference markets are thin.

The --pivot-window-s >= bucket width refusal only covers _1d/_1w/_1M (min_window is 0 for _15m/_1h/_4h), and it bounds the window, not the reference market's silence. The reset > enriched post-check catches it, but only after the write. Given the mode already renders one uncorrelated IN (SELECT …), a second term over the reference market's own days would close it in the same shape; failing that, the runbook's per-leg "measure the first reference candle" step should probably also measure the largest gap in that market over the span.


4. packages/enrichment-worker/tests/post_run_0228_it.rs:336_1m and _1M derive the same env-var name

version_before_var_name does strip_prefix("price_ohlcv_") then to_ascii_uppercase(), so price_ohlcv_1m and price_ohlcv_1M both produce POST_RUN_0228_VERSION_BEFORE_1M. Latent today — _1m is out of DEEP by decision (D-07) — but the docstring's rationale is "Derived, so the runbook and this file cannot drift apart", and adding a minute grain later would silently judge it against the monthly grain's recorded baseline with no error. Suffixing the raw table name (or matching the grain explicitly) avoids it.

…se in one list

Second review round on PR #311, findings 2, 3 and 4.

The pivot-leg reset candidate carried no reference-market term, so a
bucket on a rate-covered day whose XLM/USDC (or USDT/USDC) market was
silent was zeroed and never refilled, caught only by the post-check
after the write. The candidate now also requires a usable reference
candle on the bucket's UTC day, in the same table, rendered at all
three sites like the rate fragment and pinned by its own lockstep test.

The dry run still skipped four refusals, including the oracle-shadow
guard the runbook calls the campaign's likeliest blocker, because they
lived only in reset_step. Every month-independent refusal now lives in
assert_reset_is_admissible, run by the driver before enumerating months
and by reset_step, so the rehearsal refuses what the real run refuses.

The falsifier derived the same baseline variable name for _1m and _1M;
the two mechanism grains are matched explicitly.

The module header still described the pivot as close x ref_usd; it now
carries the USDC/USD factor and the snapshot dependency it introduced.
Second review round on PR #311, finding 1.

Since 0228 the enrichment pivot's only post-epoch USDC/USD rate is the
oracle worker's usd_rate snapshot of canonical USDC, a copy step that
worker treats as non-fatal. A stalled snapshot was invisible from every
existing signal: the dark-feed alarm sees rows written, the enrichment
backlog alarm sees the oracle tier still enriching, and the pivot tier's
own no-progress branch is a warning. One day later every pivot-priced
candle lands at close_usd = 0.

prices-{env}-oracle-usdc-snapshot-stalled fires when the peg-set
snapshot count stays at zero across three hourly buckets while
OracleRowsWritten climbs. Hourly because a single pass copying nothing
is normal; three hours still leaves most of the pivot's one-day bound.
Missing data is not breaching because no passes at all is the
dark-feed alarm's case. The snapshot stays non-fatal: failing it would
stop the poll and kill both tiers.

Deploying this needs the Observability stack, not only EventBridge.
Runbook Appendix B and C: the whole month-independent refusal list runs
before enumeration, dry run included; the manual reset-candidate count
carries the reference-market term; the abort-signal paragraph names the
one residual shape.

Task README: decisions 25 to 28, Issues 12, the history entry, the
operator checklist now naming the Observability stack, and spawn item 4
demoted since no refusal can fire after a FREEZE any more.
@adamkoot
adamkoot merged commit d9e25da into develop Sep 14, 2026
4 checks passed
adamkoot added a commit that referenced this pull request Sep 14, 2026
PR #311 merged to develop (d9e25da) and deployed to production on
2026-09-14: EventBridge (nine worker Lambdas) then Observability. The
deploy is verified on production: 53,453 XLM readings snapshotted into
usd_rate on the first oracle pass, both snapshot metrics published, the
stall alarm OK on real data, cleanup still disabled, no Lambda errors,
XLM- and USDT-quoted candles priced by the first enrichment pass, and
the first coarse-sweep pass in line with the ones before the deploy.

Closed with rescoped criterion 2 open: the Appendix C re-enrichment
campaign was not run. The spawn list stays a list by decision rather
than open tasks, and now carries what the deploy taught for the
campaign, including that on 2023-03-11 it moves XLM-quoted candles
further from the market because the pivot reference that day is a
dust close (0278).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants